<?php
/**
 * Esta clase emula completamente los conocidos conceptos
 * de la POO: getter y setter. Si una clase extiende a
 * DynamicObject, será capaz de definir sus propios
 * getters & setters, además de poder incorporar propiedades
 * dinámicamente, sólo haciendo referencias a ellas.
 *
 * @class
 */
class DynamicObject extends Core
{
	// $fields[Nombre] -> ['valor'] Valor
	//                 -> [GETTER] Callback
	//                 -> [SETTER] Callback
	//                 -> [READONLY] Callback
	//
	private $fields = array();

	const GETTER = "get";
	const SETTER = "set";
	const READONLY = "readOnly";
	const VALUE = "value";

	public function __get( $propertyName )
	{
		if ( $this->exists($propertyName) )
		{
			$this->callEventListener($propertyName, self::GETTER);
		}

		return( $this->propertyName );
	}

	public function __set( $propertyName, $value )
	{
		if ( !$this->exists($propertyName) )
		{
			$this->fields[$propertyName] = array();
			$this->fields[$propertyName][self::READONLY] = false;
		}
		else {
			if ( $this->callEventListener($propertyName, self::SETTER) ||
				 $this->isReadOnly($propertyName) )
			{
				// Se canceló el evento o la propiedad es de sólo-lectura.

				return;
			}
		}

		$this->$propertyName = $value;
	}

	public function exists( $propertyName )
	{
		return( property_exists($this, $propertyName) );
	}

	public function isReadOnly( $propertyName )
	{
		if ( !$this->exists($propertyName) ) {
			return(null);
		}

		return( $this->fields[$propertyName][self::READONLY] );
	}

	/**
	 * Establece una propiedad como sólo-lectura. Cuando una
	 * propiedad es sólo-lectura, se podrá cambiar su valor
	 * sólo -una vez- al crearla, luego funcionará como una
	 * constante.
	 *
	 * @param propName Nombre de la propiedad a setear como sólo-lectura.
	 */
	public function setPropReadOnly($propName) {
		// La propiedad no existe.
		//
		if ( !isset($this->fields[$propName][self::VALUE]) ) {
			return;
		}

		$this->fields[$propName][self::READONLY] = true;
	}

	public function hasEventListener( $propertyName, $type )
	{
		$hasListener = $this->fields[$propertyName][self::$type] ? true : false;

		return( $hasListener );
	}

	public function callEventListener( $propertyName, $type )
	{
		$cancel = false;

		if ( $this->hasEventListener($propertyName, $type) )
		{
			$this->fields[ $propertyName ][self::$type](
				$propertyName, $cancel, $this->caller
			);

			return( $cancel );
		}
	}

	/**
	 * Vincula una propiedad con una función proxy, que será llamada
	 * cada vez que se intente establecer u obtener la propiedad.
	 *
	 * Las variables disponibles en esta función son:
	 *
	 * @var $propName Nombre de la propiedad vinculada.
	 * @var $cancel   Si es true, el evento será cancelado y no se harán cambios.
	 * @var $stack    Stack frame del objeto que accedió a la propiedad.
	 *
	 * @param propertyName Nombre de la propiedad vinculada.
	 * @param callback     Función vinculada con los eventos.
	 * @param type         Define en qué caso se llamará a la función.
	 * @param readOnly     Si es true, la función vinculada no podrá ser cambiada.
	 */
	public function setPropListener( $propertyName, $callback, $type = null, $readOnly = false )
	{
		if ( !$this->exists($propertyName) ) {
			return;
		}

		// Crea la función a partir de un string.
		//
		if ( is_string($callback) ) {
			$callback = create_function('$propName,$cancel=false,$stack=null', $callback);
		}

		// No es una función válida.
		//
		if ( !is_callable($callback, true) ) {
			return;
		}

		if ( ($type != self::GETTER) && ($type != self::SETTER) ) {
			$type = self::GETTER;
		}

		// Si es sólo lectura, no se volverá a establecer.
		//
		if ( $this->hasEventListener($propertyName) &&
			 $this->isReadOnly($propertyName) )
		{
			return;
		}

		$this->fields[$propertyName][$type] = $callback;
	}

	/**
	 * Convierte el objeto actual a JSON.
	 */
	public function toJSON()
	{
		return( CoreLib::json_encode( $this->toObject() ) );
	}

	/**
	 * Convierte el objeto actual a un array asociativo.
	 */
	public function toHash()
	{
		return( (array)$this );
	}

	/**
	 * Convierte el objeto actual en un objeto intrínseco de PHP.
	 */
	public function toObject()
	{
		$object = (object) array();

		foreach( $this as $field => $value )
		{
			$object->$field = $this->$value;
		}

		return( $object );
	}


	/**
	 * Agrega propiedades y métodos a este objeto,
	 * copiándolos desde un objeto definido por el
	 * usuario.
	 *
	 * @param source Objeto desde el cual se copiará la interfaz.
	 */
	public function fromObject( &$source )
	{
		foreach( $source as $property => $value )
		{
			$this->$property = $value;
		}
	}

	/**
	 * Agrega propiedades y métodos a este objeto, copiándolos
	 * desde un array asociativo definido por el usuario.
	 *
	 * @param source Array desde el cual se copiará la interfaz.
	 */
	public function fromArray(array $source)
	{
		foreach( array_keys($source) as $item )
		{
			if ( !is_string($item) )
				continue;

			$this->$item = $source[$item];
		}
	}

	/**
	 * Crea nuevos propiedades y métodos desde un objeto,
	 * pero -solamente- copia aquéllas propiedades y métodos
	 * que se encuentren en el objeto-plantilla especificado. De
	 * esta manera, se pueden definir templates que limiten
	 * los datos accesibles y utilizar esta función para copiar
	 * sólo la información permitida.
	 *
	 * @param baseObject Objeto que se utilizará de plantilla.
	 * @param source     Objeto con los datos que se copiarán a la plantilla.
	 */
	public function fromMergeObject( $baseObject, $source )
	{
		foreach($source as $property => $value)
		{
			if ( property_exists($baseObject, $property) )
			{
				$this->$property = $value;
			}
		}
	}

	/**
	 * Crea nuevos propiedades y métodos desde un array asociativo,
	 * pero -solamente- copia aquéllas propiedades y métodos que
	 * se encuentren en el objeto-plantilla especificado. De esta
	 * forma se pueden definir templates que limiten los datos
	 * accesibles y utilizar esta función para copiar sólo la
	 * información permitida.
	 *
	 * @param baseObject Objeto que se utilizará de plantilla.
	 * @param source     Array asociativo con los datos que se copiarán a la plantilla.
	 */
	public function fromMergeArray( $baseObject, $source )
	{
		if ( !is_array($source) )
			return;

		foreach(array_keys($source) as $item)
		{
			if ( !is_string($item) )
				continue;

			if ( property_exists($baseObject, $item) )
			{
				$this->$item = $source[$item];
			}
		}
	}
}
?>
